Chenyangqi's blog

自定义Androd Gradle plugin

2021-07-05

一、Android Gradle plugin 简介

Android工程打包编译流程如下图所示,Android Studio通过com.android.tools.build这个Gradle构建工具包自动执行和管理构建流程,把源代码和资源文件最终打包成apk。同时也提供了灵活的自定义构建方式,开发者可以通过自定义Gradle插件去执行相关操作,也就是在下图中的Compilers->Dex File(s)这一过程中,把上一步的构建产物作为原材料执行自己想要的操作并输出产物作为下一步构建过程的原材料,这一点有点像Gradle Task,在这个过程中可以修改,增加和删除class文件

build-process_2x.png

二、Android Gradle plugin 工程创建

2.1、创建AGP工程

  • 1、 在项目跟目录创建以buildSrc命名的文件夹,buildSrc这个名字为默认指定的AGP项目名,不能使用其他名字命名,
  • 2、在buildSrc下创建一个build.gradle文件,创建main目录用于存代码和资源文件,可以使用Groovy和kotlin开发,如下是使用groovy开发的build.gradle的配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    apply plugin: 'groovy'

    // 声明仓库的地址
    repositories {
    jcenter()
    google()
    }

    // 声明依赖的包
    dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation "com.android.tools.build:gradle:4.1.3"
    }
  • 3、自定义类继承Plugin<Project>,所谓Gradle插件的入口,并实现apply(Project project)方法,通过project可以获得项目信息和构建产物,在可以在apply中注册Transform
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class RouterPlugin implements Plugin<Project> {

    //注入插件逻辑
    @Override
    void apply(Project project) {
    //注册 Transform
    if (project.plugins.hasPlugin(AppPlugin)) {
    AppExtension appExtension = project.extensions.getByType(AppExtension)
    Transform transform = new RouterMappingTransform()
    appExtension.registerTransform(transform)
    }
    }
    }
  • 4、自定义Transform,实现修改,增加,删除class文件
    如下定义了RouterMappingTransform继承自Transform
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class RouterMappingTransform extends Transform {

    /**
    * 当前 Transform 的名称
    * @return
    */
    @Override
    String getName() {
    return "RouterMappingTransform"
    }

    /**
    * 返回告知编译器,当前Transform需要消费的输入类型
    * 在这里是CLASS类型
    * @return
    */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
    }

    /**
    * 告知编译器,当前Transform需要收集的范围
    * @return
    */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
    * 是否支持增量
    * 通常返回False
    * @return
    */
    @Override
    boolean isIncremental() {
    return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation)

    }
    }

2.2、Transform

Transform是一个生产着消费者的模式,可以通过getInputTypes()和getScopes()指定传入的原材料类型和范围,例如上面的代码的意思就是传入的整个项目的class文件。
而Transform的传入和输出需要相对应,如果传入了整个项目的class文件,而没有输出的话那么最终生成的apk包中的dex中也就不会有class文件,如下遍历第三方jar和目录中的class文件,然后必须通过FileUtils.copyDirectory(directoryInput.file, destDir)输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
RouterMappingCollector collector = new RouterMappingCollector()

// 遍历所有的输入
transformInvocation.inputs.each {
// 把 文件夹 类型的输入,拷贝到目标目录
it.directoryInputs.each { directoryInput ->
def destDir = transformInvocation.outputProvider
.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
collector.collect(directoryInput.file)
FileUtils.copyDirectory(directoryInput.file, destDir)
}

// 把 JAR 类型的输入,拷贝到目标目录
it.jarInputs.each { jarInput ->
def dest = transformInvocation.outputProvider
.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes, Format.JAR)
collector.collectFromJarFile(jarInput.file)
FileUtils.copyFile(jarInput.file, dest)
}
}

println("${getName()} all mapping class name = " + collector.mappingClassName)

File mappingJarFile = transformInvocation.outputProvider.
getContentLocation(
"router_mapping",
getOutputTypes(),
getScopes(),
Format.JAR)

println("${getName()} mappingJarFile = $mappingJarFile")

if (mappingJarFile.getParentFile().exists()) {
mappingJarFile.getParentFile().mkdirs()
}

if (mappingJarFile.exists()) {
mappingJarFile.delete()
}

// 将生成的字节码,写入本地文件
FileOutputStream fos = new FileOutputStream(mappingJarFile)
JarOutputStream jarOutputStream = new JarOutputStream(fos)
ZipEntry zipEntry =
new ZipEntry(RouterMappingByteCodeBuilder1.CLASS_NAME + ".class")
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(
RouterMappingByteCodeBuilder1.get(collector.mappingClassName))
jarOutputStream.closeEntry()
jarOutputStream.close()
fos.close()
}

3.3、定义DSL

通过定义DSL,外部的调用放可以定义参数传递到我们的自定义Gradle插件中,android项目中build.gradle中就定义我们很熟悉的DSL,如下的android标签便是com.android.tools.build中定义的DSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"

defaultConfig {
applicationId "com.chenyangqi.app"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

例如我们要定义一个这样的DSL

1
2
3
router {
wikiDir "某个文件目录"
}

那么就可以在buildSrc工程中定义

1
2
3
class RouterExtension {
String wikiDir
}

然后再Plugin中获取DSL中传入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RouterPlugin implements Plugin<Project> {

//注入插件逻辑
@Override
void apply(Project project) {
...
//注册扩展参数
project.getExtensions().create("router", RouterExtension)

println("int RouterPlugin,apply from ${project.name}")
//当配置阶段结束时获取设置的wikiDir
project.afterEvaluate {
RouterExtension extension = project["router"]
println("设置的保存wikiDir路径:${extension.wikiDir}")
}
}
}

三、Android Gradle plugin 使用

3.1、作为本项目使用

作为本项目使用,不需要做其他配置,需要在application module中定义DSL(如果Gradle插件有定义DSL的话)

3.2、上传Maven仓库,作为第三方插件提供

如果插件需要上传maven仓库,提供第三方使用的,需要添加uploadArchives task,并指定groupId、artifactId、version,并通过url指定上传的maven仓库地址,如下buildSrc中的build.gradle如下图,url没有使用maven仓库,而是一个本地目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apply plugin: 'groovy'

// 声明仓库的地址
repositories {
jcenter()
google()
}

// 声明依赖的包
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:4.1.3"
}

// 调用maven插件,用于发布
apply plugin: 'maven'

// 配置maven插件中的uploadArchives任务
uploadArchives {
repositories {
mavenDeployer {
// 设置发布路径为 工程根目录下面的 repo 文件夹
repository(url: uri('../repo')) {
// 设置groupId,通常为包名
pom.groupId = 'com.chenyangqi.router'
// 设置artifactId,为当前插件的名称
pom.artifactId = 'router-gradle-plugin'
// 设置 插件的版本号
pom.version = '1.0.0'
}
}
}
}

需要注意的是buildSrc并不能执行uploadArchives任务,可以拷贝buildSrc到同级目录中并重命名他,例如命令为pluginSrc,然后就可以通过执行pulginSrc下的uploadArchives任务完成插件发布,发布命令:./gradlew :pluginSrc:uploadArchives

四、 Gradle插件开发中遇到的坑

1
2
Unable to instantiate appComponentFactory
java.lang.ClassNotFoundException: Didn't find class "androidx.core.app.CoreComponentFactory" on path: DexPathList[[zip file "/data/app/~~81QZSgXA9N_0MNnG9dfHDg==/com.example.as_c_demo-B8o2lngQ5IM_LEQpSLsP9w==/base.apk"],nativeLibraryDirectories=[/data/app/~~81QZSgXA9N_0MNnG9dfHDg==/com.example.as_c_demo-B8o2lngQ5IM_LEQpSLsP9w==/lib/arm64, /data/app/~~81QZSgXA9N_0MNnG9dfHDg==/com.example.as_c_demo-B8o2lngQ5IM_LEQpSLsP9w==/base.apk!/lib/arm64-v8a, /system/lib64, /system/system_ext/lib64]]

这个问题在Gradle插件开发时困要我一两天,打包出来的apk中缺少很多class文件,运行时报classNotFoundException,造成这个的原因就是在Transform中,没有完整的把提供的class原材料,全部输出
所以不要忘记执行 FileUtils.copyFile(jarInput.file, dest)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
RouterMappingCollector collector = new RouterMappingCollector()

// 遍历所有的输入
transformInvocation.inputs.each {
// 把 文件夹 类型的输入,拷贝到目标目录
it.directoryInputs.each { directoryInput ->
def destDir = transformInvocation.outputProvider
.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
collector.collect(directoryInput.file)
FileUtils.copyDirectory(directoryInput.file, destDir)
}

// 把 JAR 类型的输入,拷贝到目标目录
it.jarInputs.each { jarInput ->
def dest = transformInvocation.outputProvider
.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes, Format.JAR)
collector.collectFromJarFile(jarInput.file)
FileUtils.copyFile(jarInput.file, dest)
}
}
}

另一个坑是升级Android studio到Arctic Fox强制使用Gradle 7.0后,很多api过时,造成项目运行失败,我暂时没有去适配gradle 7.0,gradle插件工程未适配7.0前,谨慎设计,关于Gradle 7.0在Gradle插件中的适配可以参考google官方文档
https://developer.android.com/studio/releases/gradle-plugin#7-0-0

五、 使用kotlin开发Gradle

Gradle 5.0开始支持kotlin开发gradle插件,如果不熟悉groovy可以参考如下配置,配置build.kotlin.kts就能够使用kotlin开发
项目根目录build.gradle添加kotlin插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
buildscript {
ext.kotlin_version = "1.4.20"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20'
}
}

allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

buildSrc中创建build.gradle.kts,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

plugins {
`kotlin-dsl`
}

kotlinDslPluginOptions {
experimentalWarning.set(false)
}

repositories {
jcenter()
google()
gradlePluginPortal()
mavenCentral()
}

dependencies {
implementation(gradleApi())
implementation("com.android.tools.build", "gradle", "4.1.3")
}

gradlePlugin {
plugins {
create("RouterPlugin") {
// 自定义id名称,其他模块通过此id引用插件
id = "com.chenyangqi.router.plugin"
// 指向对应的 插件实现类
implementationClass = "com.chenyangqi.router.plugin.RouterPlugin"
}
}
}

自定义Gradle参考源码

可通过此项目中buildSrc了解gradle插件开发
自定义Android Gradle插件,实现编译期路由采集

本文为博主原创,转载请注明出处

Tags: Android

扫描二维码,分享此文章